// 时间工具类
// 该包提供了一个自定义的时间类型 HTime，用于处理 JSON 序列化/反序列化、数据库操作中的时间格式化问题。
// 使用此类型可以确保时间在 JSON 和数据库中以统一的格式 "2006-01-02 15:04:05" 进行存储和传输。
package utils

// 导入所需的包
import (
	"database/sql/driver" // 用于实现数据库驱动的 Value 和 Scanner 接口
	"fmt"                 // 用于格式化字符串和错误信息
	"time"                // Go 语言内置的时间包，提供时间处理功能
)

// HTime 是一个自定义的时间类型，它基于 time.Time 进行了封装。
// 通过内嵌 time.Time，HTime 继承了 time.Time 的所有方法和字段（如 Year(), Month(), Day() 等），
// 同时允许我们为 HTime 定义特定的行为，比如自定义 JSON 和数据库的序列化/反序列化方式。
// 注意：HTime 是一个结构体类型，包含一个匿名的 time.Time 字段。
type HTime struct {
	time.Time // 内嵌 time.Time，使得 HTime 拥有 time.Time 的所有能力
}

// formatTime 是一个全局变量，定义了时间格式化的标准格式字符串。
// 该格式遵循 Go 语言 time 包的 "纪念日格式"：2006-01-02 15:04:05。
// 这个特定的格式字符串代表了 Go 语言诞生的时间（Mon Jan 2 15:04:05 MST 2006），因此被用作模板。
// 在整个包中，所有的时间格式化和解析都将使用这个格式。
var (
	formatTime = "2006-01-02 15:04:05"
)

// MarshalJSON 实现了 json.Marshaler 接口。
// 当使用 json.Marshal() 函数序列化包含 HTime 类型字段的结构体时，会自动调用此方法。
// 目的：将 HTime 类型的时间值转换为符合预设格式的 JSON 字符串。
// 步骤：
// 1. 调用 t.Format(formatTime) 将内部的 time.Time 格式化为 "2006-01-02 15:04:05" 形式的字符串。
// 2. 使用 fmt.Sprintf() 在格式化后的时间字符串两端加上双引号，使其成为有效的 JSON 字符串值。
// 3. 将这个带引号的字符串转换为字节切片 []byte，因为 json.Marshaler 接口要求返回 []byte。
// 4. 返回字节切片和 nil 错误（因为格式化操作不会失败）。
// 示例输出：如果时间是 2023-10-01 12:00:00，则返回 []byte(`"2023-10-01 12:00:00"`)。
func (t HTime) MarshalJSON() ([]byte, error) {
	// 将时间格式化为指定格式，并用双引号包围，形成 JSON 字符串
	formatted := fmt.Sprintf("\"%s\"", t.Format(formatTime))
	// 将字符串转换为字节切片并返回，同时返回 nil 表示没有错误
	return []byte(formatted), nil
}

// UnmarshalJSON 实现了 json.Unmarshaler 接口。
// 当使用 json.Unmarshal() 函数将 JSON 数据反序列化到 HTime 类型的变量时，会调用此方法。
// 接收者是指针类型 (*HTime)，因为反序列化需要修改接收者本身的值。
// 目的：将 JSON 字符串（如 `"2023-10-01 12:00:00"`）解析并赋值给 HTime 对象。
// 步骤：
// 1. 参数 data 是原始的 JSON 字节数据，通常是一个用双引号包围的时间字符串。
// 2. 使用 time.ParseInLocation() 函数解析这个字符串。
//   - 格式字符串是 `"` + formatTime + `"`，即 `"2006-01-02 15:04:05"`，以匹配输入的 JSON 字符串格式。
//   - string(data) 将字节切片转换为字符串。
//   - time.Local 指定使用本地时区进行解析。
//
// 3. 如果解析成功，得到一个 time.Time 类型的 now 变量。
// 4. 如果解析失败，err 会被设置，函数直接返回该错误。
// 5. 解析成功后，通过 *t = HTime{Time: now} 将解析得到的时间赋值给调用此方法的 HTime 指针所指向的对象。
// 6. 返回 nil 表示成功。
// 注意：此方法修改了接收者 t 指向的 HTime 实例。
func (t *HTime) UnmarshalJSON(data []byte) (err error) {
	// 尝试按照指定格式和本地时区解析传入的 JSON 字节数据
	now, err := time.ParseInLocation(`"`+formatTime+`"`, string(data), time.Local)
	// 如果解析过程中出现错误（例如格式不匹配），则直接返回该错误
	if err != nil {
		return err
	}
	// 将解析成功的时间赋值给接收者指针指向的 HTime 对象
	*t = HTime{Time: now}
	// 返回 nil 表示反序列化成功
	return nil
}

// Value 实现了 driver.Valuer 接口。
// 当将 HTime 类型的值传递给数据库驱动（例如，在 SQL INSERT 或 UPDATE 语句中作为参数）时，会调用此方法。
// 目的：将 HTime 对象转换为数据库驱动可以理解的值。
// 步骤：
// 1. 创建一个零值 time.Time 对象（表示时间上的零点）。
// 2. 比较当前 HTime 对象内部的时间 (t.Time) 与零值时间的 Unix 纳秒时间戳。
//   - 如果两者相等，说明 HTime 存储的是一个零时间或未初始化的时间。
//     3. 如果是零值，则返回 nil 和 nil 错误。nil 在数据库中通常表示 NULL 值。
//     4. 如果不是零值，则直接返回内部的 time.Time 对象和 nil 错误。
//     数据库驱动会知道如何处理 time.Time 类型。
//
// 注意：此方法的接收者是值类型 (HTime)，因为我们只需要读取值，不需要修改它。
func (t HTime) Value() (driver.Value, error) {
	// 创建一个表示零时间的 time.Time 变量
	var zeroTime time.Time
	// 比较当前时间的 Unix 纳秒时间戳是否与零时间的时间戳相等
	if t.Time.UnixNano() == zeroTime.UnixNano() {
		// 如果是零值，返回 nil（表示数据库中的 NULL）和 nil 错误
		return nil, nil
	}
	// 如果不是零值，返回内部的 time.Time 值和 nil 错误
	return t.Time, nil
}

// Scan 实现了 sql.Scanner 接口。
// 当从数据库读取一行数据，并将时间列的值扫描到 HTime 类型的变量时，会调用此方法。
// 接收者是指针类型 (*HTime)，因为需要修改 HTime 实例本身。
// 目的：将数据库驱动提供的值（通常是 time.Time）赋值给 HTime 对象。
// 步骤：
// 1. 参数 v 是数据库驱动提供的原始值，类型为 interface{}。
// 2. 使用类型断言 v.(time.Time) 尝试将 v 转换为 time.Time 类型。
// 3. 如果类型断言成功 (ok 为 true)，说明数据库中的值是期望的时间类型。
// 4. 将转换成功的时间值赋给接收者指针指向的 HTime 对象：*t = HTime{Time: value}。
// 5. 返回 nil 表示扫描成功。
// 6. 如果类型断言失败 (ok 为 false)，说明数据库中的值不是 time.Time 类型，无法处理。
// 7. 此时返回一个格式化的错误，说明无法将该值转换为时间戳。
// 注意：此方法修改了接收者 t 指向的 HTime 实例。
func (t *HTime) Scan(v interface{}) error {
	// 尝试将传入的接口值 v 断言为 time.Time 类型
	value, ok := v.(time.Time)
	// 如果断言成功
	if ok {
		// 将断言成功的时间值赋值给接收者指针指向的 HTime 对象
		*t = HTime{Time: value}
		// 返回 nil 表示扫描成功，没有错误
		return nil
	}
	// 如果断言失败，返回一个错误，说明无法转换该值
	return fmt.Errorf("can not convert %v to timestamp", v)
}
